package br.unifor.cct.mia.util.missingvalue;

import java.awt.Point;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.Enumeration;
import java.util.List;

import weka.core.Instance;
import weka.core.Instances;
import weka.core.Utils;
import br.unifor.cct.mia.coevolution.InvalidTypeException;
import br.unifor.cct.mia.dataenhancement.Database;
import br.unifor.cct.mia.dataenhancement.GenotypeConverter;
import br.unifor.cct.mia.dataenhancement.Structure;
import br.unifor.cct.mia.dataenhancement.Tuple;
import br.unifor.cct.mia.evaluate.Evaluate;
import br.unifor.cct.mia.evaluate.classification.WekaClassification;
import br.unifor.cct.mia.evolutionary.SpeciesConstants;
import br.unifor.cct.mia.util.LoadFile;
import br.unifor.cct.mia.util.Methods;
import br.unifor.cct.mia.util.SaveReport;

/**
 * Uses normalized Euclidean distance to 
 * find the training instance closest to the given test instance, and predicts
 * the same class as this training instance. If multiple instances have
 * the same (smallest) distance to the test instance, the first one found is used.
 * Aha, D., and D. Kibler (1991) \"Instance-based learning algorithms\", 
 * Machine Learning, vol.6, pp. 37-66.
 */
public class WekaEuclideanDistance {

	private static String[] datasets = {"breast","car","dermatology","diabetes","iris","zoo"};
	private static int porcentagens[] = {5,10,20,33};
	private static int pastas[] = {1,2,3};
	
	private static Integer[] learnerTypes = {Evaluate.J48, Evaluate.IBK, 
									Evaluate.NAIVE_BAYES, Evaluate.PART};
	private static Integer[] kValues = {1,3,5};
	private static FileWriter resultFile;

	private Instances m_Train;
	private double [] m_MinArray;
	private double [] m_MaxArray;

	public static void main(String[] args) throws IOException {	

		DecimalFormat df = new DecimalFormat("##.##");

		for (String dbName: datasets) {

			for ( int p:pastas) {

				String path = "./Convertidas/"+dbName+"/Estratificado/20/MV"+p+"/";
				String strucFile = path+dbName+"_struc.txt";
				String testFilePath = path+dbName+"_data_test.txt";		

				Structure st = LoadFile.loadStructure(strucFile);
				Database dbTeste = LoadFile.loadDatabase(testFilePath);

				String resultFilePath = path+dbName+"_RESULTADO_KNN.txt";	
				resultFile = new FileWriter(resultFilePath);

				for ( int kValue:kValues ) {

					for ( int m:porcentagens ) {

						for ( int n=1; n<=5; n++ ) {

							String dbFile = null;
							if ( m==5 )
								dbFile = path+dbName+"_data_MV-0"+m+"_"+n+".txt";
							else
								dbFile = path+dbName+"_data_MV-"+m+"_"+n+".txt";


							Date d0 = new Date();

							Database db = LoadFile.loadDatabase(dbFile);

							List positions = LoadFile.getFaultDatabase(st, db);
							Tuple tuples = new Tuple();
							db.mountStr(tuples, positions);

							File file;
							String[] valoresFinal = new String[positions.size()];

							int knnIndex[] = new int[kValue];
							double knnValues[] = new double[kValue];

							try {
								file = new GenotypeConverter().convert(null,
										SpeciesConstants.FULL_DATABASE,
										"temp/resultMVFull.txt",
										st,db,null,null);

								Instances instances = new Instances(new BufferedReader(new FileReader(file)));
								instances.setClassIndex(st.size()-1);

								WekaEuclideanDistance euclidean = new WekaEuclideanDistance(instances);

								for (int i=0;i<positions.size();i++) {
									Arrays.fill(knnIndex, Integer.MAX_VALUE);
									Arrays.fill(knnValues, Integer.MAX_VALUE);
									Point point = (Point)positions.get(i);

									Instance inst = instances.instance(point.x+1);

									for( int j=0;j<instances.numInstances(); j++ ) {
										if ( j!=point.x+1 ) {
											Instance second = instances.instance(j);
											double dist = euclidean.distance(inst, second);

											double maxValue[] = Methods.arrayMaximunValuePos(knnValues);									
											double minDist = maxValue[0];
											int minIndex = (int)maxValue[1];

											if ( dist<minDist ) {
												knnValues[minIndex] = dist;
												knnIndex[minIndex] = j;
											}
										}
									}


									if ( !inst.attribute(point.y).isNominal() ){

										double sun = 0;
										for( int minIndex:knnIndex ) {
											Instance nn = instances.instance(minIndex);	
											sun += nn.value(point.y);
										}
										valoresFinal[i] = String.valueOf( (double)((double)sun/(double)kValue) );

									}
									else {

										int[] count = new int[kValue];
										List<String> values = new ArrayList<String>();

										for( int minIndex:knnIndex ) {
											Instance nn = instances.instance(minIndex);									
											String v = nn.stringValue(point.y);									
											if (!values.contains(v)) values.add(v);

											int index = values.indexOf(v);
											count[index]++;
										}

										/*for( int minIndex:knnIndex ) {
										Instance nn = instances.instance(minIndex);									
										String v = nn.stringValue(point.y);									
										int index = values.indexOf(v);
										count[index]++;
									}*/

										int pos = Methods.arrayMaximunPos(count);
										valoresFinal[i] = values.get(pos);

									}					

								}
							} catch (IOException e) {
								e.printStackTrace();
							} catch (InvalidTypeException e) {
								e.printStackTrace();
							}


							try { 
								SaveReport report = new SaveReport("temp/result_mv.txt",st.getStrucFile());
								report.addLine(tuples.stringWithValues(positions, valoresFinal, st));
								report.saveToDisk();

								for ( Integer learnerType: learnerTypes ) {
									WekaClassification classificator = new WekaClassification(learnerType,null);
									double result = classificator.evaluate(report.getFile());

									GenotypeConverter converter = new GenotypeConverter();
									File fileTest = converter.fullConvert("temp/resultFinalTeste_MV.txt", st, dbTeste);

									classificator = new WekaClassification(learnerType,null);
									double test = classificator.evaluate(report.getFile(),fileTest);


									Date d1 = new Date();

									String line = "[" + getEvaluateValue(learnerType) + "];" 
									+ "[k=" + kValue + "];"
									+ "[m=" + m + "];"
									+ "[n=" + n + "];"
									+ df.format(result) + ";"
									+ df.format(test) + ";"
									+ String.valueOf((d1.getTime() - d0.getTime()) / 1000);

									System.out.println(line);

									resultFile.write(line+"\n");
									resultFile.flush();
								}
							}
							catch (Exception e) {
								e.printStackTrace();
							}
						}
					}
				}	
				resultFile.close();
			}
		}
	}

	public WekaEuclideanDistance(Instances instances) {
		m_Train = new Instances(instances, 0, instances.numInstances());
		m_Train.deleteWithMissingClass();

		m_MinArray = new double [m_Train.numAttributes()];
		m_MaxArray = new double [m_Train.numAttributes()];
		for (int i = 0; i < m_Train.numAttributes(); i++) {
			m_MinArray[i] = m_MaxArray[i] = Double.NaN;
		}
		Enumeration enu = m_Train.enumerateInstances();
		while (enu.hasMoreElements()) {
			updateMinMax((Instance) enu.nextElement());
		}
	}

	public double distance(Instance first, Instance second) {
		updateMinMax(first);
		double diff, distance = 0;

		for(int i = 0; i < m_Train.numAttributes(); i++) { 
			if (i == m_Train.classIndex()) {
				continue;
			}
			if (m_Train.attribute(i).isNominal()) {

				// If attribute is nominal
				if (first.isMissing(i) || second.isMissing(i) ||
						((int)first.value(i) != (int)second.value(i))) {
					distance += 1;
				}
			} else {

				// If attribute is numeric
				if (first.isMissing(i) || second.isMissing(i)){
					if (first.isMissing(i) && second.isMissing(i)) {
						diff = 1;
					} else {
						if (second.isMissing(i)) {
							diff = norm(first.value(i), i);
						} else {
							diff = norm(second.value(i), i);
						}
						if (diff < 0.5) {
							diff = 1.0 - diff;
						}
					}
				} else {
					diff = norm(first.value(i), i) - norm(second.value(i), i);
				}
				distance += diff * diff;
			}
		}

		return distance;
	}


	private double norm(double x,int i) {

		if (Double.isNaN(m_MinArray[i])
				|| Utils.eq(m_MaxArray[i], m_MinArray[i])) {
			return 0;
		} else {
			return (x - m_MinArray[i]) / (m_MaxArray[i] - m_MinArray[i]);
		}
	}

	private void updateMinMax(Instance instance) {
		for (int j = 0;j < m_Train.numAttributes(); j++) {
			if ((m_Train.attribute(j).isNumeric()) && (!instance.isMissing(j))) {
				if (Double.isNaN(m_MinArray[j])) {
					m_MinArray[j] = instance.value(j);
					m_MaxArray[j] = instance.value(j);
				} else {
					if (instance.value(j) < m_MinArray[j]) {
						m_MinArray[j] = instance.value(j);
					} else {
						if (instance.value(j) > m_MaxArray[j]) {
							m_MaxArray[j] = instance.value(j);
						}
					}
				}
			}
		}
	}
	private static String getEvaluateValue(int value) {

		String result = "";
		switch (value) {
		case 1:
			result = "J48";
			break;
		case 2:
			result = "MULTILAYER_PERCEPTRON";
			break;
		case 3:
			result = "NAIVE_BAYES";
			break;
		case 4:
			result = "IBK";
			break;
		case 5:
			result = "PART";
			break;
		case 6:
			result = "RBF_NETWORK";
			break;
		}
		return result;
	}
}
